import {
  CENTEREVENTS,
  ColorSchema,
  CSDashLine,
  CSLine,
  CSPoint,
  EventsRes,
  ICurve,
  KeepSizeObject3D,
  POINT_TYPE,
  Tuple3,
} from '@amcax/base';
import { sketchEventCenter } from '@app-base/environments/nextcad/events/SketchEventCenter';
import { ctx2 } from '@app-base/module';
import {
  Box3,
  Euler,
  Matrix4,
  Object3D,
  Shape,
  Sphere,
  Vector3,
  Vector3Tuple,
} from 'three-legacy';
import { Vector2Tuple } from 'three/src/Three.Core.js';

export class SketchTransformTool extends Object3D {
  center: Vector3;
  frameMatrix = new Matrix4();
  frame: Tuple3<Vector3Tuple>;

  selfTranslate = new Vector3();
  selfRotate = 0;
  selfScale = 1;

  objectContainer = new Object3D();
  operationContainer: KeepSizeObject3D;

  get selfMatrix() {
    const translate = new Matrix4().makeTranslation(
      ...this.selfTranslate.toArray(),
    );
    const rotate = new Matrix4().makeRotationAxis(
      new Vector3(0, 0, 1),
      this.selfRotate,
    );

    return translate.multiply(rotate);
  }

  zDir: Vector3;
  xDir: Vector3;
  yDir: Vector3;

  pickObjByMouse(e: EventsRes<PointerEvent>) {
    const rayCaster = ctx2.viewer.sceneSetup.createRaycaster(
      e.mouse.x,
      e.mouse.y,
    );

    const result = rayCaster.intersectObjects([
      this.centerPoint,
      this.xScalePoint,
      this.yScalePoint,
      this.xAxis,
      this.yAxis,
      this.rotateLine,
    ]);

    const pickedObject = result?.[0]?.object;

    return pickedObject;
  }

  pickedObject: Object3D;
  draggingObject: Object3D;
  dragStart: Vector3;

  mouseMoveListener(e: EventsRes<PointerEvent>) {
    if (this.draggingObject == null) {
      const oldObj = this.pickedObject;
      this.pickedObject = this.pickObjByMouse(e);

      if (this.pickedObject !== oldObj) {
        this.updateHighLight(oldObj, 'default');
        this.updateHighLight(this.pickedObject, 'hover');
      }

      ctx2.container.style.cursor =
        this.pickedObject == null ? 'auto' : 'pointer';

      this.onAllowMarqueeChange(this.pickedObject == null);
    } else {
      const sceneSetup = ctx2.viewer.sceneSetup;

      const point = sceneSetup
        .screenToWorld(e.mouse.x, e.mouse.y, this.zDir, this.position)
        .clone();

      const xDir = this.xDir
        .clone()
        .applyMatrix4(
          new Matrix4().makeRotationAxis(this.zDir, this.selfRotate),
        );

      const yDir = this.yDir
        .clone()
        .applyMatrix4(
          new Matrix4().makeRotationAxis(this.zDir, this.selfRotate),
        );

      if (
        this.draggingObject === this.xScalePoint ||
        this.draggingObject === this.yScalePoint
      ) {
        const isX = this.draggingObject === this.xScalePoint;

        const center = this.position.clone();
        const finalVector = new Vector3().subVectors(point, center);
        const oldVector = new Vector3().subVectors(this.dragStart, center);
        const oldDistance = oldVector.dot(isX ? xDir : yDir);
        const finalDistance = finalVector.dot(isX ? xDir : yDir);

        const extraScale = finalDistance / oldDistance;

        this.onScale(extraScale * this.oldScale);
        this.applyScale(extraScale * this.oldScale);
      }

      if (
        this.draggingObject === this.xAxis ||
        this.draggingObject === this.yAxis ||
        this.draggingObject === this.centerPoint
      ) {
        const diff = new Vector3().subVectors(point, this.dragStart);

        let direction: Vector2Tuple;

        if (this.draggingObject === this.xAxis) {
          direction = [
            diff.dot(xDir) * Math.cos(this.selfRotate),
            diff.dot(xDir) * Math.sin(this.selfRotate),
          ];
        }

        if (this.draggingObject === this.yAxis) {
          direction = [
            diff.dot(yDir) * Math.cos(this.selfRotate + Math.PI / 2),
            diff.dot(yDir) * Math.sin(this.selfRotate + Math.PI / 2),
          ];
        }

        if (this.draggingObject === this.centerPoint) {
          direction = [
            diff.dot(xDir) * Math.cos(this.selfRotate) +
              diff.dot(yDir) * Math.cos(this.selfRotate + Math.PI / 2),
            diff.dot(xDir) * Math.sin(this.selfRotate) +
              diff.dot(yDir) * Math.sin(this.selfRotate + Math.PI / 2),
          ];
        }

        this.applyTranslate([
          this.oldTranslate.x + direction[0],
          this.oldTranslate.y + direction[1],
        ]);
      }

      if (this.draggingObject === this.rotateLine) {
        const center = this.position.clone();

        const getAngle = (targetPoint: Vector3) => {
          const vector = new Vector3().subVectors(targetPoint, center);
          let angle = vector.angleTo(this.xDir);

          if (vector.dot(this.yDir) < 0) {
            angle = Math.PI * 2 - angle;
          }

          return angle;
        };

        const angleDiff = getAngle(point) - getAngle(this.dragStart);

        this.applyRotate(angleDiff + this.oldRotate);
      }
    }
  }
  _mouseMoveListener: (e: EventsRes<PointerEvent>) => void;

  mouseUpListener() {
    this.draggingObject = null;
    this.dragStart = null;
    this.oldTranslate = null;
    this.oldRotate = null;
    this.oldScale = null;
    this.onAllowMarqueeChange(this.pickedObject == null);
  }
  _mouseUpListener: (e: EventsRes<PointerEvent>) => void;

  oldTranslate: Vector3;
  oldRotate: number;
  oldScale: number;

  mouseDownListener(e: EventsRes<PointerEvent>) {
    const sceneSetup = ctx2.viewer.sceneSetup;
    if (e.button.onLeft && this.pickedObject != null) {
      this.draggingObject = this.pickedObject;

      this.dragStart = sceneSetup
        .screenToWorld(e.mouse.x, e.mouse.y, this.zDir, this.position)
        .clone();

      this.oldTranslate = this.selfTranslate.clone();
      this.oldRotate = this.selfRotate;
      this.oldScale = this.selfScale;

      this.onAllowMarqueeChange(false);
    }
  }
  _mouseDownListener: (e: EventsRes<PointerEvent>) => void;

  updateHighLight(object: Object3D, type: 'default' | 'hover') {
    if (
      object === this.centerPoint ||
      object === this.xScalePoint ||
      object === this.yScalePoint
    ) {
      (object as CSPoint).setType(
        type === 'default' ? POINT_TYPE.DEFAULT : POINT_TYPE.SELECT_POINT,
      );
    }

    if (object === this.xAxis) {
      this.xAxis.setColor(
        {
          default: 0xff0000,
          hover: ColorSchema.hover.axis,
          select: ColorSchema.select.axis,
        }[type],
      );
    }

    if (object === this.yAxis) {
      this.yAxis.setColor(
        {
          default: 0x00ff00,
          hover: ColorSchema.hover.axis,
          select: ColorSchema.select.axis,
        }[type],
      );
    }

    if (object === this.rotateLine) {
      this.rotateLine.setColor(
        {
          default: ColorSchema.normal.edge,
          hover: ColorSchema.hover.edge,
          select: ColorSchema.select.edge,
        }[type],
      );
    }

    ctx2.requestRender();
  }

  addMouseListener() {
    this._mouseMoveListener = this.mouseMoveListener.bind(this);
    sketchEventCenter().on(CENTEREVENTS.POINTERMOVE, this._mouseMoveListener);
    this._mouseUpListener = this.mouseUpListener.bind(this);
    sketchEventCenter().on(CENTEREVENTS.POINTERUP, this._mouseUpListener);
    this._mouseDownListener = this.mouseDownListener.bind(this);
    sketchEventCenter().on(CENTEREVENTS.POINTERDOWN, this._mouseDownListener);
  }
  removeMouseListener() {
    sketchEventCenter().off(CENTEREVENTS.POINTERMOVE, this._mouseMoveListener);
    sketchEventCenter().off(CENTEREVENTS.POINTERUP, this._mouseUpListener);
    sketchEventCenter().off(CENTEREVENTS.POINTERDOWN, this._mouseDownListener);
  }

  updateFrameMatrix() {
    const frame = sketchEventCenter().getSketchInfo()
      .frame as Tuple3<Vector3Tuple>;
    this.frame = frame;

    const origin = frame[0] as Vector3Tuple;
    this.zDir = new Vector3(...frame[1]);
    this.xDir = new Vector3(...frame[2]);
    this.yDir = new Vector3().crossVectors(this.zDir, this.xDir);

    this.frameMatrix
      .makeTranslation(...origin)
      .multiply(
        new Matrix4()
          .set(
            this.xDir.x,
            this.xDir.y,
            this.xDir.z,
            0,
            this.yDir.x,
            this.yDir.y,
            this.yDir.z,
            0,
            this.zDir.x,
            this.zDir.y,
            this.zDir.z,
            0,
            0,
            0,
            0,
            1,
          )
          .invert(),
      )
      .multiply(new Matrix4().makeTranslation(...this.center.toArray()));
  }

  initRender() {
    this.centerPoint = new CSPoint({
      vertex: new Vector3(),
      pointType: POINT_TYPE.DEFAULT,
    });
    this.operationContainer.add(this.centerPoint);

    this.xScalePoint = new CSPoint({
      vertex: new Vector3(12, 0),
      pointType: POINT_TYPE.DEFAULT,
    });
    this.operationContainer.add(this.xScalePoint);

    this.yScalePoint = new CSPoint({
      vertex: new Vector3(0, 12),
      pointType: POINT_TYPE.DEFAULT,
    });
    this.operationContainer.add(this.yScalePoint);

    this.xAxis = new CSLine({
      vertexs: [new Vector3(), new Vector3(10, 0)],
      color: 0xff0000,
    });
    this.operationContainer.add(this.xAxis);

    this.yAxis = new CSLine({
      vertexs: [new Vector3(), new Vector3(0, 10)],
      color: 0x00ff00,
    });
    this.operationContainer.add(this.yAxis);

    const shape = new Shape().absarc(0, 0, 8, 0, Math.PI / 2, false);

    const vertices = shape
      .getPoints(25)
      .map((v) => new Vector3(...v.toArray()));

    this.rotateLine = new CSLine({
      vertexs: vertices,
    });

    this.operationContainer.add(this.rotateLine);
  }

  constructor(
    sketchObjects: Object3D[],
    public onScale: (scale: number) => void,
    public onAllowMarqueeChange: (allow: boolean) => void,
  ) {
    super();
    this.add(this.objectContainer);
    this.operationContainer = new KeepSizeObject3D(
      ctx2.viewer.sceneSetup.camera,
      true,
    );
    this.add(this.operationContainer);

    sketchObjects.forEach((c) =>
      this.objectContainer.add((c as CSDashLine | CSPoint).cloneSelf()),
    );
    this.initRender();

    this.addMouseListener();

    const box = new Box3();
    sketchObjects.forEach((o) => {
      const info = o.userData.info as ICurve;
      info.vertices.forEach((v) => box.expandByPoint(new Vector3(...v)));
    });

    const sphere = new Sphere();
    box.getBoundingSphere(sphere);
    this.center = sphere.center;

    this.updateFrameMatrix();

    this.updateFullMatrix();
  }

  centerPoint: CSPoint;
  xAxis: CSLine;
  yAxis: CSLine;
  rotateLine: CSLine;
  xScalePoint: CSPoint;
  yScalePoint: CSPoint;

  updateFullMatrix() {
    this.position.set(0, 0, 0);
    this.setRotationFromEuler(new Euler());

    this.applyMatrix4(
      new Matrix4().multiplyMatrices(this.frameMatrix, this.selfMatrix),
    );

    this.objectContainer.position.set(0, 0, 0);
    this.objectContainer.setRotationFromEuler(new Euler());
    this.objectContainer.scale.set(1, 1, 1);
    this.objectContainer.applyMatrix4(
      new Matrix4()
        .makeScale(this.selfScale, this.selfScale, this.selfScale)
        .multiply(this.frameMatrix.clone().invert()),
    );
  }

  applyTranslate(v: Vector2Tuple) {
    this.selfTranslate = new Vector3(...v);

    this.updateFullMatrix();
    ctx2.requestRender();
  }

  applyRotate(angle: number) {
    this.selfRotate = angle;
    this.updateFullMatrix();
    ctx2.requestRender();
  }

  applyScale(scale: number) {
    this.selfScale = scale;

    this.updateFullMatrix();
    ctx2.requestRender();
  }

  destory() {
    this.parent?.remove(this);
    this.removeMouseListener();
    ctx2.container.style.cursor = 'auto';
  }
}
